2012-09-03 23:17:00
上一篇我们简单学习了下ATL 的继承链处理。可是,如果要裸写一个含内嵌IE控件的窗口,还是要写一个很长的QueryInterface,以及AddRef和Release,确保引用计数的正确性。于是我们不得不参考ATL的COM_TNTERFACE的处理技巧,来达到一定程度上的易用性。
首先,除了IUnknown以外,其余所有涉及到的接口,均按上一篇的形式,弄成相应的IXXXImpl,这部分代码见: http://xllib.codeplex.com/SourceControl/changeset/view/19617#315460 细节不再赘述。现在关键是IUnknown的处理: 首先,如果对象继承自两个或以上的COM接口,要保证所有的查询IUnknown的地方都会返回同一个IUnknown*。 其次,在同一对象中,无论哪个接口调用AddRef和Release,都修改同一个引用计数。
基于以上两点,各个IXXXImpl不能自己去实现IUnknown的这三个函数,就是return E_NOTIMPL也不行。对于COM接口来说,IUnknown必须进行有意义的实现。
就拿WebBrowser的例子来说,WebBrowser容器至少实现IOleClientSite、IOleInPlaceSite、IOleInPlaceFrame,通常还会实现IDocHostUIHandler、DWebBrowserEvents2,它们的继承关系是:
如果看虚函数表,将会是这样:
| 接口 | 来自于 | 来自于 | 来自于 |
|---|---|---|---|
| IUnknown | IOleClientSite | ||
| IOleClientSite | IOleClientSite | ||
| IUnknown | IOleWindow | IOleInPlaceSite | |
| IOleWindow | IOleInPlaceSite | ||
| IOleInPlaceSite | IOleInPlaceSite | ||
| IUnknown | IOleWindow | IOleInPlaceUIWindow | IOleInPlaceFrame |
| IOleWindow | IOleInPlaceUIWindow | IOleInPlaceFrame | |
| IOleInPlaceUIWindow | IOleInPlaceFrame | ||
| IOleInPlaceFrame | IOleInPlaceFrame | ||
| IUnknown | IDocHostUIHandler | ||
| IDocHostUIHandler | IDocHostUIHandler | ||
| IUnknown | IDispatch (DWebBrowserEvents2) | ||
| IDispatch (DWebBrowserEvents2) | IDispatch (DWebBrowserEvents2) |
其中出现了5个IUnknown,我们要在最底层对象上一次性的实现了,而不是在中间层实现。 如果我们裸写QueryInterface,就像是上次一样,会是这个样子:
1HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppvObject)
2{
3 *ppvObject = nullptr;
4
5 if (riid == IID_IUnknown)
6 {
7 *ppvObject = (IOleClientSite *)this;
8 }
9 else if (riid == IID_IOleInPlaceSite)
10 {
11 *ppvObject = (IOleInPlaceSite *)this;
12 }
13 else if (riid == IID_IOleInPlaceUIWindow)
14 {
15 *ppvObject = (IOleInPlaceUIWindow *)this;
16 }
17 else if (riid == IID_IOleInPlaceFrame)
18 {
19 *ppvObject = (IOleInPlaceFrame *)this;
20 }
21 else if (riid == IID_IDocHostUIHandler)
22 {
23 *ppvObject = (IDocHostUIHandler *)this;
24 }
25 else if (riid == IID_IDispatch)
26 {
27 *ppvObject = (IDispatch *)this;
28 }
29 else if (riid == DIID_DWebBrowserEvents2)
30 {
31 *ppvObject = (DWebBrowserEvents2 *)this;
32 }
33
34 if (*ppvObject == nullptr)
35 {
36 return E_NOINTERFACE;
37 }
38
39 AddRef();
40 return S_OK;
41}
注意高亮的那一句,查询IUnknown接口时,我们指定了IOleClientSite对应的IUnknown,也就是表中最前面的那个IUnknown。
很容易想到的一个做法是:
1#define COM_INTERFACE(i) \
2 \
3 if (riid == __uuidof(i)) \
4 { \
5 *ppvObject = (i *)this; \
6 AddRef(); \
7 return S_OK; \
8 }
然后前后用 BEGIN、END宏定义,拼凑成完整的QueryInterface。可是这没有解决IUnknown的问题,如果查询IUnknown,“(i *)this”会有歧义。这种定义下,代码上无法知道“第一个接口”是啥,无法写出一个可行的this到IUnknown*的转换。
不会了,于是就抄ATL。ATL中是列了一张表,我简化一下,每一个项的结构定义是:
1struct InterfaceEntry
2{
3 const IID *piid;
4 DWORD_PTR dwOffset;
5};
然后定义如下函数:
1typedef c ComClass;
2
3static const InterfaceEntry *GetEntries()
4{
5 static const InterfaceEntry entries[] =
6 {
7 { &__uuidof(i), (DWORD_PTR)((i *)(ComClass *)8) - 8 },
8 { &__uuidof(i), (DWORD_PTR)((i *)(ComClass *)8) - 8 },
9 // ...
10 { nullptr, 0 }
11 };
12
13 return entries;
14}
其中c是最终那个对象的类,i是每一个要对外暴露的接口。每一项的第二列存了该接口到类的首地址的偏移量。至于那个8,为什么要用8,是不是他们拍脑袋的?用别常数有没有问题呢……谁来告诉我?(在下面的实际实现中我用了sizeof(nullptr),不知道有木有问题。)
为此,我们定义一个额外的辅助类:
1template <typename T>
2class ComClass
3{
4public:
5 ComClass() : m_nRefCount(0)
6 {
7 InternalAddRef();
8 }
9
10 ~ComClass()
11 {
12
13 }
14
15public:
16 STDMETHODIMP InternalQueryInterface(const InterfaceEntry *pEntries, REFIID riid, LPVOID *ppvObject)
17 {
18 *ppvObject = nullptr;
19 T *pThis = (T *)this;
20
21 IUnknown *pUnknown = (IUnknown *)((INT_PTR)pThis + pEntries->dwOffset);
22
23 if (riid == __uuidof(IUnknown))
24 {
25 *ppvObject = pUnknown;
26 pUnknown->AddRef();
27 return S_OK;
28 }
29
30 for (const InterfaceEntry *pEntry = pEntries; pEntry->piid != nullptr; ++pEntry)
31 {
32 if (riid == *pEntry->piid)
33 {
34 *ppvObject = (IUnknown *)((INT_PTR)pThis + pEntry->dwOffset);
35 pUnknown->AddRef();
36 return S_OK;
37 }
38 }
39
40 return E_NOINTERFACE;
41 }
42
43 ULONG STDMETHODCALLTYPE InternalAddRef()
44 {
45 return (ULONG)InterlockedIncrement(&m_nRefCount);
46 }
47
48 ULONG STDMETHODCALLTYPE InternalRelease()
49 {
50 LONG nRefCount = InterlockedDecrement(&m_nRefCount);
51
52 if (nRefCount <= 0)
53 {
54 delete this;
55 }
56
57 return (ULONG)nRefCount;
58 }
59
60protected:
61 LONG m_nRefCount;
62};
它来对三个IUnknown的接口作实际的实现,其中InternalQueryInterface完成从Entries里面找到偏移量,然后算出接口地址返回给外界的过程。然后……规定:所有COM类必须继承刚才这个ComClass!
现在可以来定义COM_INTERFACE以及它的BEGIN、END宏了:
1#define XL_COM_INTERFACE_BEGIN(c) \
2 \
3 typedef c ComClass; \
4 \
5 static const InterfaceEntry *GetEntries() \
6 { \
7 static const InterfaceEntry entries[] = \
8 { \
9
10#define XL_COM_INTERFACE(i) \
11 \
12 { &__uuidof(i), (DWORD_PTR)((i *)(ComClass *)sizeof(nullptr)) - sizeof(nullptr) }, \
13
14#define XL_COM_INTERFACE_END() \
15 \
16 { nullptr, 0 } \
17 }; \
18 \
19 return entries; \
20 } \
21 \
22 STDMETHODIMP QueryInterface(REFIID riid, LPVOID *ppvObject) \
23 { \
24 return InternalQueryInterface(GetEntries(), riid, ppvObject); \
25 } \
26 \
27 ULONG STDMETHODCALLTYPE AddRef() \
28 { \
29 return InternalAddRef(); \
30 } \
31 \
32 ULONG STDMETHODCALLTYPE Release() \
33 { \
34 return InternalRelease(); \
35 } \
其实主要就是在拼凑那张表,其余都是直接带上的。
上面的代码位于:
http://xllib.codeplex.com/SourceControl/changeset/view/19617#315452
http://xllib.codeplex.com/SourceControl/changeset/view/19617#315458
现在来做OleContainer(http://xllib.codeplex.com/SourceControl/changeset/view/19617#315455)。
1class OleContainerImpl : public IOleClientSiteImpl<>,
2 public IOleInPlaceSiteImpl<>,
3 public IOleInPlaceFrameImpl<>
4{
5public:
6 OleContainerImpl() : m_hOleParent(nullptr),
7 m_pStorage(nullptr),
8 m_pOleObj(nullptr),
9 m_pInPlaceObj(nullptr),
10 m_bInPlaceActived(false)
11 {
12 ZeroMemory(&m_rect, sizeof(RECT));
13
14 OleInitialize(nullptr);
15 }
16
17 virtual ~OleContainerImpl()
18 {
19 DestroyOleObject();
20
21 OleUninitialize();
22 }
23
24public:
25 bool CreateOleObject(const IID &clsid)
26 {
27 DestroyOleObject();
28
29 HRESULT hr = StgCreateDocfile(nullptr,
30 STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_DIRECT | STGM_CREATE,
31 0,
32 &m_pStorage);
33 if (FAILED(hr))
34 {
35 return false;
36 }
37
38 hr = OleCreate(clsid, IID_IOleObject, OLERENDER_DRAW, 0, this, m_pStorage, (LPVOID *)&m_pOleObj);
39
40 if (FAILED(hr))
41 {
42 return false;
43 }
44
45 hr = m_pOleObj->QueryInterface(IID_IOleInPlaceObject, (LPVOID *)&m_pInPlaceObj);
46
47 if (FAILED(hr))
48 {
49 return false;
50 }
51
52 return true;
53 }
54
55 void DestroyOleObject()
56 {
57 if (m_pInPlaceObj != nullptr)
58 {
59 m_pInPlaceObj->Release();
60 m_pInPlaceObj = nullptr;
61 }
62
63 if (m_pOleObj != nullptr)
64 {
65 m_pOleObj->Release();
66 m_pOleObj = nullptr;
67 }
68
69 if (m_pStorage != nullptr)
70 {
71 m_pStorage->Release();
72 m_pStorage = nullptr;
73 }
74 }
75
76 bool InPlaceActive(HWND hWnd, LPCRECT lpRect = nullptr)
77 {
78 if (hWnd == nullptr || m_pOleObj == nullptr)
79 {
80 return false;
81 }
82
83 m_hOleParent = hWnd;
84
85 if (lpRect != nullptr)
86 {
87 CopyMemory(&m_rect, lpRect, sizeof(RECT));
88 }
89 else
90 {
91 GetClientRect(m_hOleParent, &m_rect);
92 }
93
94 HRESULT hr = m_pOleObj->DoVerb(OLEIVERB_INPLACEACTIVATE, nullptr, this, 0, m_hOleParent, &m_rect);
95
96 if (FAILED(hr))
97 {
98 return false;
99 }
100
101 return true;
102 }
103
104public:
105 STDMETHOD(GetWindow)(HWND *phwnd)
106 {
107 *phwnd = m_hOleParent;
108 return S_OK;
109 }
110
111 STDMETHOD(CanInPlaceActivate)()
112 {
113 return m_bInPlaceActived ? S_FALSE : S_OK;
114 }
115
116 STDMETHOD(GetWindowContext)(IOleInPlaceFrame **ppFrame,
117 IOleInPlaceUIWindow **ppDoc,
118 LPRECT lprcPosRect,
119 LPRECT lprcClipRect,
120 LPOLEINPLACEFRAMEINFO lpFrameInfo)
121 {
122 if (m_hOleParent == nullptr)
123 {
124 return E_NOTIMPL;
125 }
126
127 *ppFrame = (IOleInPlaceFrame*)this;
128 (*ppFrame)->AddRef();
129
130 *ppDoc = NULL;
131
132 CopyMemory(lprcPosRect, &m_rect, sizeof(RECT));
133 CopyMemory(lprcClipRect, &m_rect, sizeof(RECT));
134
135 lpFrameInfo->cb = sizeof(OLEINPLACEFRAMEINFO);
136 lpFrameInfo->fMDIApp = false;
137 lpFrameInfo->hwndFrame = GetParent(m_hOleParent);
138 lpFrameInfo->haccel = nullptr;
139 lpFrameInfo->cAccelEntries = 0;
140
141 return S_OK;
142 }
143
144protected:
145 HWND m_hOleParent;
146 IStorage *m_pStorage;
147 IOleObject *m_pOleObj;
148 IOleInPlaceObject *m_pInPlaceObj;
149 bool m_bInPlaceActived;
150 RECT m_rect;
151};
152
153class OleContainer : public ComClass<OleContainer>,
154 public OleContainerImpl
155{
156public:
157 OleContainer()
158 {
159
160 }
161
162 ~OleContainer()
163 {
164 DestroyOleObject();
165 }
166
167public:
168 XL_COM_INTERFACE_BEGIN(OleContainer)
169 XL_COM_INTERFACE(IOleClientSite)
170 XL_COM_INTERFACE(IOleInPlaceSite)
171 XL_COM_INTERFACE(IOleInPlaceFrame)
172 XL_COM_INTERFACE_END()
173};
对于InPlaceActive,之前理解有误,OleCreate的时候,并不需要给出窗口,到InPlaceActive之前的瞬间给出即可。而CanInPlaceActive,之前是根据窗口句柄是否为空来确定S_OK还是S_FALSE,这有错误。快要InPlaceActive了,于是去把m_hWnd设上,InPlaceActive去调用CanInPlaceActive,CanInPlaceActive发现窗口句柄有了,于是拒绝……所以形成了我在《裸写一个含内嵌IE控件的窗口》中的尴尬局面。按照上面的实现,逻辑上就没问题了。
另外,这里拆成两个,是为了让后面的WebBrowser继承OleContainerImpl,而OleContainer自己又可以独立使用。
还有需要注意的是,OleContainer的析构函数里最好主动调用一下父类的资源释放函数,不然,等子类完全析构后再自动调用到父类的析构函数,子类(子类部分)已经完全消亡了,但是QueryInterface之类的本来是定义在子类的,这时候可能会出现纯虚函数调用的错误。下同。
再来做WebBrowser(http://xllib.codeplex.com/SourceControl/changeset/view/19615#315453):
1class WebBrowserImpl : public OleContainerImpl,
2 public IDocHostUIHandlerImpl<>,
3 public DWebBrowserEvents2Impl<>
4{
5public:
6 WebBrowserImpl() : m_pWebBrowser(nullptr),
7 m_pCPC(nullptr),
8 m_pCP(nullptr)
9 {
10
11 }
12
13 ~WebBrowserImpl()
14 {
15 DestroyWebBrowser();
16 }
17
18public:
19 bool CreateWebBrowser(HWND hWnd, LPCRECT lpRect = nullptr)
20 {
21 DestroyWebBrowser();
22
23 if (!CreateOleObject(__uuidof(::WebBrowser)))
24 {
25 return false;
26 }
27
28 if (!InPlaceActive(hWnd, lpRect))
29 {
30 return false;
31 }
32
33 HRESULT hr = m_pOleObj->QueryInterface(__uuidof(IWebBrowser2), (LPVOID *)&m_pWebBrowser);
34
35 if (FAILED(hr))
36 {
37 return false;
38 }
39
40 hr = m_pWebBrowser->QueryInterface(__uuidof(IConnectionPointContainer), (LPVOID *)&m_pCPC);
41
42 if (FAILED(hr))
43 {
44 return false;
45 }
46
47 hr = m_pCPC->FindConnectionPoint(__uuidof(DWebBrowserEvents2), &m_pCP);
48
49 if (FAILED(hr))
50 {
51 return false;
52 }
53
54 DWORD dwCookie = 0;
55 hr = m_pCP->Advise((DWebBrowserEvents2 *)this, &dwCookie);
56
57 if (FAILED(hr))
58 {
59 return false;
60 }
61
62 return true;
63 }
64
65 void DestroyWebBrowser()
66 {
67 if (m_pCP != nullptr)
68 {
69 m_pCP->Release();
70 m_pCP = nullptr;
71 }
72
73 if (m_pCPC != nullptr)
74 {
75 m_pCPC->Release();
76 m_pCPC = nullptr;
77 }
78
79 if (m_pWebBrowser != nullptr)
80 {
81 m_pWebBrowser->Release();
82 m_pWebBrowser = nullptr;
83 }
84
85 DestroyOleObject();
86 }
87
88protected:
89 IWebBrowser2 *m_pWebBrowser;
90 IConnectionPointContainer *m_pCPC;
91 IConnectionPoint *m_pCP;
92};
93
94class WebBrowser : public ComClass<WebBrowser>,
95 public WebBrowserImpl
96{
97public:
98 WebBrowser()
99 {
100
101 }
102
103 ~WebBrowser()
104 {
105 DestroyWebBrowser();
106 }
107
108public:
109 XL_COM_INTERFACE_BEGIN(WebBrowser)
110 XL_COM_INTERFACE(IOleClientSite)
111 XL_COM_INTERFACE(IOleInPlaceSite)
112 XL_COM_INTERFACE(IOleInPlaceFrame)
113 XL_COM_INTERFACE(IDocHostUIHandler)
114 XL_COM_INTERFACE(DWebBrowserEvents2)
115 XL_COM_INTERFACE_END()
116};
这里很普通,没什么要说的。现在就可以跟原来一样使用了:
Main.cpp
1class WebBrowser : public xl::WebBrowser
2{
3public:
4 void Navigate(LPCTSTR lpUrl)
5 {
6 BSTR bstrUrl = SysAllocString(lpUrl);
7 m_pWebBrowser->Navigate(bstrUrl, nullptr, nullptr, nullptr, nullptr);
8 SysFreeString(bstrUrl);
9 }
10};
11
12LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
13{
14 switch (message)
15 {
16 case WM_DESTROY:
17 PostQuitMessage(0);
18 break;
19 default:
20 return DefWindowProc(hWnd, message, wParam, lParam);
21 }
22
23 return 0;
24}
25
26int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
27 _In_opt_ HINSTANCE hPrevInstance,
28 _In_ LPTSTR lpCmdLine,
29 _In_ int nCmdShow)
30{
31 UNREFERENCED_PARAMETER(hPrevInstance);
32 UNREFERENCED_PARAMETER(lpCmdLine);
33
34 const LPCTSTR CLASS_NAME = _T("WebBrowserContainer");
35
36 WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
37 wcex.style = CS_HREDRAW | CS_VREDRAW;
38 wcex.lpfnWndProc = WndProc;
39 wcex.cbClsExtra = 0;
40 wcex.cbWndExtra = 0;
41 wcex.hInstance = hInstance;
42 wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
43 wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
44 wcex.lpszClassName = CLASS_NAME;
45
46 RegisterClassEx(&wcex);
47
48 HWND hWnd = CreateWindow(CLASS_NAME,
49 _T("WebBrowser Sample"),
50 WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
51 CW_USEDEFAULT,
52 0,
53 CW_USEDEFAULT,
54 0,
55 nullptr,
56 nullptr,
57 hInstance,
58 nullptr);
59
60 if (hWnd == nullptr)
61 {
62 return 0;
63 }
64
65 ShowWindow(hWnd, nCmdShow);
66 UpdateWindow(hWnd);
67
68 WebBrowser wb;
69
70 if (!wb.CreateWebBrowser(hWnd))
71 {
72 return 0;
73 }
74
75 wb.Navigate(_T("http://www.baidu.com/"));
76
77 MSG msg = {};
78
79 while (GetMessage(&msg, nullptr, 0, 0))
80 {
81 if (!TranslateAccelerator(msg.hwnd, nullptr, &msg))
82 {
83 TranslateMessage(&msg);
84 DispatchMessage(&msg);
85 }
86 }
87
88 return (int)msg.wParam;
89}
运行界面:

和原来一样。例子代码见WebBrowserSample2.rar(http://pan.baidu.com/s/1c0q4skK)。
首发:http://www.cppblog.com/Streamlet/archive/2012/09/03/189321.html